/**
 * @file fbdev.c
 *
 */

/*********************
 *      INCLUDES
 *********************/
#include "lcd/fbdev.h"

#include <fcntl.h>
#include <linux/fb.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>

/*********************
 *      DEFINES
 *********************/

#define FBDEV_PATH "/dev/fb0"

#ifndef DIV_ROUND_UP
#define DIV_ROUND_UP(n, d) (((n) + (d)-1) / (d))
#endif

/**********************
 *      TYPEDEFS
 **********************/

/**********************
 *  STATIC VARIABLES
 **********************/

static struct fb_var_screeninfo vinfo;
static struct fb_fix_screeninfo finfo;

static char* fbp = 0;
static long int screensize = 0;
static int fbfd = 0;

/**********************
 *      MACROS
 **********************/

/**********************
 *   GLOBAL FUNCTIONS
 **********************/

void fbdev_init(void)
{
	// Open the file for reading and writing
	fbfd = open(FBDEV_PATH, O_RDWR);
	if (fbfd == -1) {
		perror("Error: cannot open framebuffer device");
		return;
	}
	LV_LOG_INFO("The framebuffer device was opened successfully");

	// Make sure that the display is on.
	if (ioctl(fbfd, FBIOBLANK, FB_BLANK_UNBLANK) != 0) {
		perror("ioctl(FBIOBLANK)");
		return;
	}

	// Get fixed screen information
	if (ioctl(fbfd, FBIOGET_FSCREENINFO, &finfo) == -1) {
		perror("Error reading fixed information");
		return;
	}

	// Get variable screen information
	if (ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo) == -1) {
		perror("Error reading variable information");
		return;
	}

	// LV_LOG_INFO("%dx%d, %dbpp", vinfo.xres, vinfo.yres, vinfo.bits_per_pixel);
	printf("%dx%d, %dbpp\n", vinfo.xres, vinfo.yres, vinfo.bits_per_pixel);

	// Figure out the size of the screen in bytes
	screensize = finfo.smem_len; // finfo.line_length * vinfo.yres;

	// Map the device to memory
	fbp = (char*)mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0);
	if ((intptr_t)fbp == -1) {
		perror("Error: failed to map framebuffer device to memory");
		return;
	}

	// Don't initialise the memory to retain what's currently displayed / avoid clearing the screen.
	// This is important for applications that only draw to a subsection of the full framebuffer.

	LV_LOG_INFO("The framebuffer device was mapped to memory successfully");
}

void fbdev_exit(void)
{
	close(fbfd);
}

/**
 * Flush a buffer to the marked area
 * @param drv pointer to driver where this function belongs
 * @param area an area where to copy `color_p`
 * @param color_p an array of pixels to copy to the `area` part of the screen
 */
void fbdev_flush(lv_disp_drv_t* drv, const lv_area_t* area, lv_color_t* color_p)
{
	if (fbp == NULL || area->x2 < 0 || area->y2 < 0 || area->x1 > (int32_t)vinfo.xres - 1 || area->y1 > (int32_t)vinfo.yres - 1) {
		lv_disp_flush_ready(drv);
		return;
	}

	/*Truncate the area to the screen*/
	int32_t act_x1 = area->x1 < 0 ? 0 : area->x1;
	int32_t act_y1 = area->y1 < 0 ? 0 : area->y1;
	int32_t act_x2 = area->x2 > (int32_t)vinfo.xres - 1 ? (int32_t)vinfo.xres - 1 : area->x2;
	int32_t act_y2 = area->y2 > (int32_t)vinfo.yres - 1 ? (int32_t)vinfo.yres - 1 : area->y2;

	lv_coord_t w = (act_x2 - act_x1 + 1);
	long int location = 0;
	long int byte_location = 0;
	unsigned char bit_location = 0;

	/*32 or 24 bit per pixel*/
	if (vinfo.bits_per_pixel == 32 || vinfo.bits_per_pixel == 24) {
		uint32_t* fbp32 = (uint32_t*)fbp;
		int32_t y;
		for (y = act_y1; y <= act_y2; y++) {
			location = (act_x1 + vinfo.xoffset) + (y + vinfo.yoffset) * finfo.line_length / 4;
			memcpy(&fbp32[location], (uint32_t*)color_p, (act_x2 - act_x1 + 1) * 4);
			color_p += w;
		}
	}
	/*16 bit per pixel*/
	else if (vinfo.bits_per_pixel == 16) {
		uint16_t* fbp16 = (uint16_t*)fbp;
		int32_t y;
		for (y = act_y1; y <= act_y2; y++) {
			location = (act_x1 + vinfo.xoffset) + (y + vinfo.yoffset) * finfo.line_length / 2;
			memcpy(&fbp16[location], (uint32_t*)color_p, (act_x2 - act_x1 + 1) * 2);
			color_p += w;
		}
	}
	/*8 bit per pixel*/
	else if (vinfo.bits_per_pixel == 8) {
		uint8_t* fbp8 = (uint8_t*)fbp;
		int32_t y;
		for (y = act_y1; y <= act_y2; y++) {
			location = (act_x1 + vinfo.xoffset) + (y + vinfo.yoffset) * finfo.line_length;
			memcpy(&fbp8[location], (uint32_t*)color_p, (act_x2 - act_x1 + 1));
			color_p += w;
		}
	}
	/*1 bit per pixel*/
	else if (vinfo.bits_per_pixel == 1) {
		uint8_t* fbp8 = (uint8_t*)fbp;
		int32_t x;
		int32_t y;
		for (y = act_y1; y <= act_y2; y++) {
			for (x = act_x1; x <= act_x2; x++) {
				location = (x + vinfo.xoffset) + (y + vinfo.yoffset) * vinfo.xres;
				byte_location = location / 8; /* find the byte we need to change */
				bit_location = location % 8; /* inside the byte found, find the bit we need to change */
				fbp8[byte_location] &= ~(((uint8_t)(1)) << bit_location);
				fbp8[byte_location] |= ((uint8_t)(color_p->full)) << bit_location;
				color_p++;
			}

			color_p += area->x2 - act_x2;
		}
	} else {
		/*Not supported bit per pixel*/
	}

	// May be some direct update command is required
	// ret = ioctl(state->fd, FBIO_UPDATE, (unsigned long)((uintptr_t)rect));

	lv_disp_flush_ready(drv);
}

void fbdev_get_sizes(uint32_t* width, uint32_t* height, uint32_t* dpi)
{
	if (width)
		*width = vinfo.xres;

	if (height)
		*height = vinfo.yres;

	if (dpi && vinfo.height)
		*dpi = DIV_ROUND_UP(vinfo.xres * 254, vinfo.width * 10);
}

void fbdev_set_offset(uint32_t xoffset, uint32_t yoffset)
{
	vinfo.xoffset = xoffset;
	vinfo.yoffset = yoffset;
}